﻿// SilverlightSerializer by Mike Talbot
//                          http://whydoidoit.com
//                          email:   mike.talbot@alterian.com
//                          twitter: mike_talbot
//
// This code is free to use, no warranty is offered or implied.
// If you redistribute, please retain this header.

#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

#endregion


namespace Win8nl.Utilities
{
  /// <summary>
  ///   Indicates that a property or field should not be serialized
  /// </summary>
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
  public class DoNotSerialize : Attribute
  {

  }
  /// <summary>
  /// Used in checksum mode to flag a property as not being part
  /// of the "meaning" of an object - i.e. two objects with the
  /// same checksum "mean" the same thing, even if some of the
  /// properties are different, those properties would not be
  /// relevant to the purpose of the object
  /// </summary>
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
  public class DoNotChecksum : Attribute
  {
  }
  /// <summary>
  /// Attribute used to flag IDs this can be useful for check object
  /// consistence when the serializer is in a mode that does not 
  /// serialize identifiers
  /// </summary>
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
  public class SerializerId : Attribute
  {
  }

  public interface ISerializeObject
  {
    object[] Serialize(object target);
    object Deserialize(object[] data);
  }

  [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
  public class SerializerAttribute : Attribute
  {
    internal Type SerializesType;
    public SerializerAttribute(Type serializesType)
    {
      SerializesType = serializesType;
    }
  }

  /// <summary>
  ///   Silverlight/.NET compatible binary serializer with suppression support
  ///   produces compact representations, suitable for further compression
  /// </summary>
  public static class SilverlightSerializer
  {
    private static readonly Dictionary<Type, IEnumerable<FieldInfo>> FieldLists = new Dictionary<Type, IEnumerable<FieldInfo>>();
    private static readonly Dictionary<string, IEnumerable<PropertyInfo>> PropertyLists = new Dictionary<string, IEnumerable<PropertyInfo>>();
    private static readonly Dictionary<string, IEnumerable<PropertyInfo>> ChecksumLists = new Dictionary<string, IEnumerable<PropertyInfo>>();
    [ThreadStatic]
    private static List<Type> _knownTypes;
    [ThreadStatic]
    private static Dictionary<object, int> _seenObjects;
    [ThreadStatic]
    private static List<object> _loadedObjects;
    [ThreadStatic]
    private static List<string> _propertyIds;

    [ThreadStatic]
    private static Stack<List<object>> _loStack;
    [ThreadStatic]
    private static Stack<Dictionary<object, int>> _soStack;
    [ThreadStatic]
    private static Stack<List<Type>> _ktStack;
    [ThreadStatic]
    private static Stack<List<string>> _piStack;
    [ThreadStatic]
    private static bool _isChecksum;
    [ThreadStatic]
    public static bool IgnoreIds;

    /// <summary>
    /// Arguments for a missing type event
    /// </summary>
    public class TypeMappingEventArgs : EventArgs
    {
      /// <summary>
      /// The missing types name
      /// </summary>
      public string TypeName = string.Empty;
      /// <summary>
      /// Supply a type to use instead
      /// </summary>
      public Type UseType = null;
    }

    /// <summary>
    /// Event that is fired if a particular type cannot be found
    /// </summary>
    public static event EventHandler<TypeMappingEventArgs> MapMissingType;


    private static void InvokeMapMissingType(TypeMappingEventArgs e)
    {
      EventHandler<TypeMappingEventArgs> handler = MapMissingType;
      if (handler != null)
        handler(null, e);
    }

    /// <summary>
    /// Put the serializer into Checksum mode
    /// </summary>
    public static bool IsChecksum
    {
      get
      {
        return _isChecksum;
      }
      set
      {
        _isChecksum = value;
      }
    }

    /// <summary>
    /// Deserialize to a type
    /// </summary>
    /// <param name="array"></param>
    /// <returns></returns>
    public static T Deserialize<T>(byte[] array) where T : class
    {
      return Deserialize(array) as T;

    }

    /// <summary>
    /// Deserialize from a stream to a type
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public static T Deserialize<T>(Stream stream) where T : class
    {
      return Deserialize(stream) as T;
    }

    /// <summary>
    /// Get a checksum for an item.  Checksums "should" be different 
    /// for every object that has a different "meaning".  You can
    /// flag properties as DoNotChecksum if that helps to keep decorative
    /// properties away from the checksum whilst including meaningful ones
    /// </summary>
    /// <param name="item">The object to checksum</param>
    /// <returns>A checksum string, this includes no illegal characters and can be used as a file name</returns>
    public static string GetChecksum(object item)
    {
      if (item == null)
        return "";
      byte[] checksum = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
      var isChecksum = SilverlightSerializer.IsChecksum;
      SilverlightSerializer.IsChecksum = true;
      var toBytes = SilverlightSerializer.Serialize(item);
      SilverlightSerializer.IsChecksum = isChecksum;

      for (var i = 0; i < toBytes.Length; i++)
      {
        checksum[i & 15] ^= toBytes[i];
      }
      return toBytes.Count().ToString() + Encode(checksum);
    }

    private static string Encode(byte[] checksum)
    {
      var s = Convert.ToBase64String(checksum);
      StringBuilder b = new StringBuilder();
      for( var i = 0; i < s.Length; i++ )
      {
        var c = s[i];
        b.Append(Char.IsLetterOrDigit(c) ? c : Char.GetNumericValue(c));
      }
      return b.ToString();
    }


    //Holds a reference to the custom serializers
    private static readonly Dictionary<Type, ISerializeObject> Serializers = new Dictionary<Type, ISerializeObject>();
    //Dictionary to ensure we only scan an assembly once
    private static readonly Dictionary<Assembly, bool> Assemblies = new Dictionary<Assembly, bool>();

    /// <summary>
    /// Register all of the custom serializers in an assembly
    /// </summary>
    /// <param name="assembly">Leave blank to register the assembly that the method is called from, or pass an assembly</param>
    public static void RegisterSerializationAssembly(Assembly assembly = null)
    {
      if (Assemblies.ContainsKey(assembly))
        return;
      Assemblies[assembly] = true;
      ScanAllTypesForAttribute((tp, attr) =>
                               {
                                 Serializers[((SerializerAttribute)attr).SerializesType] = Activator.CreateInstance(tp) as ISerializeObject;
                               }, assembly, typeof(SerializerAttribute));
    }

    //Function to be called when scanning types
    internal delegate void ScanTypeFunction(Type type, Attribute attribute);

    /// <summary>
    /// Scan all of the types in an assembly for a particular attribute
    /// </summary>
    /// <param name="function">The function to call</param>
    /// <param name="assembly">The assembly to scan</param>
    /// <param name="attribute">The attribute to look for</param>
    internal static void ScanAllTypesForAttribute(ScanTypeFunction function, Assembly assembly, Type attribute = null)
    {
      try
      {
        foreach (var tp in assembly.DefinedTypes)
        {
          if (attribute != null)
          {
            var attrs = tp.GetCustomAttributes(attribute, false);
            if (attrs != null)
            {
              foreach (var attr in attrs)
                function(tp.AsType(), attr);
            }
          }
          else
            function(tp.AsType(), null);
        }
      }
      catch (Exception)
      {


      }
    }
    /// <summary>
    /// Dictionary of all the used objects to check if properties are different
    /// to those set during construction
    /// </summary>
    private static readonly Dictionary<Type, object> Vanilla = new Dictionary<Type, object>();
    /// <summary>
    /// Write persistence debugging information to the debug output window
    /// often used with Verbose
    /// </summary>
    public static bool IsLoud;
    /// <summary>
    /// Write all types, even if they are known, often used with Loud mode
    /// </summary>
    public static bool Verbose;

    /// <summary>
    ///   Caches and returns property info for a type
    /// </summary>
    /// <param name = "itm">The type that should have its property info returned</param>
    /// <returns>An enumeration of PropertyInfo objects</returns>
    /// <remarks>
    ///   It should be noted that the implementation converts the enumeration returned from reflection to an array as this more than double the speed of subsequent reads
    /// </remarks>
    private static IEnumerable<PropertyInfo> GetPropertyInfo(Type itm)
    {
      lock (PropertyLists)
      {
        IEnumerable<PropertyInfo> ret = null;
        Debug.Assert(itm.AssemblyQualifiedName != null);
        if (!IsChecksum)
        {
          if (!PropertyLists.TryGetValue(itm.AssemblyQualifiedName, out ret))
          {
            ret = itm.GetRuntimeProperties().Where(p =>
             p.GetIndexParameters().Count() == 0 &&
             p.GetMethod != null && p.GetMethod.IsPublic &&
             p.SetMethod != null && p.SetMethod.IsPublic &&
             !p.GetMethod.IsStatic).Where(p => p.GetCustomAttributes(typeof(DoNotSerialize), false).Count() == 0).ToArray();
            PropertyLists[itm.AssemblyQualifiedName] = ret;
          }
        }
        else
        {
          if (!ChecksumLists.TryGetValue(itm.AssemblyQualifiedName, out ret))
          {
            ret = itm.GetRuntimeProperties().Where(p =>
            p.GetIndexParameters().Count() == 0 &&
             p.GetMethod != null && p.GetMethod.IsPublic &&
             p.SetMethod != null && p.SetMethod.IsPublic &&
            !p.GetMethod.IsStatic).Where(p => p.GetCustomAttributes(typeof(DoNotSerialize), false).Count() == 0).ToArray();

            ChecksumLists[itm.AssemblyQualifiedName] = ret;
          }
        }
        return IgnoreIds && ret != null
                   ? ret.Where(p => p.GetCustomAttributes(typeof(SerializerId), true).Count() == 0)
                   : ret;
      }
    }

    /// <summary>
    ///   Caches and returns field info for a type
    /// </summary>
    /// <param name = "itm">The type that should have its field info returned</param>
    /// <returns>An enumeration of FieldInfo objects</returns>
    /// <remarks>
    ///   It should be noted that the implementation converts the enumeration returned from reflection to an array as this more than double the speed of subsequent reads
    /// </remarks>
    private static IEnumerable<FieldInfo> GetFieldInfo(Type itm)
    {
      lock (FieldLists)
      {
        IEnumerable<FieldInfo> ret = null;
        if (FieldLists.ContainsKey(itm))
          ret = FieldLists[itm];
        else
        {
          ret = FieldLists[itm] = itm.GetRuntimeFields().Where(p => p.IsPublic && !p.IsStatic && !p.IsInitOnly).Where(p => p.GetCustomAttributes(typeof(DoNotSerialize), false).Count() == 0).ToArray();
        }

        return IsChecksum ? ret.Where(p => p.GetCustomAttributes(typeof(DoNotChecksum), true).Count() == 0) : ret;

      }
    }

    /// <summary>
    ///   Returns a token that represents the name of the property
    /// </summary>
    /// <param name = "name">The name for which to return a token</param>
    /// <returns>A 2 byte token representing the name</returns>
    private static ushort GetPropertyDefinitionId(string name)
    {
      lock (_propertyIds)
      {
        var ret = _propertyIds.IndexOf(name);
        if (ret >= 0)
          return (ushort)ret;
        _propertyIds.Add(name);
        return (ushort)(_propertyIds.Count - 1);
      }
    }

    /// <summary>
    /// Deserializes from a stream, potentially into an existing instance
    /// </summary>
    /// <param name="inputStream">Stream to deserialize from</param>
    /// <param name="instance">Instance to use</param>
    /// <returns></returns>
    public static object Deserialize(Stream inputStream, object instance = null)
    {
      var v = Verbose;
      CreateStacks();
      try
      {
        _ktStack.Push(_knownTypes);
        _piStack.Push(_propertyIds);
        _loStack.Push(_loadedObjects);

        var rw = new BinaryReader(inputStream);
        var version = rw.ReadString();
        var count = rw.ReadInt32();
        if (version == "SerV3")
          Verbose = rw.ReadBoolean();
        _propertyIds = new List<string>();
        _knownTypes = new List<Type>();
        _loadedObjects = new List<object>();
        for (var i = 0; i < count; i++)
        {
          var typeName = rw.ReadString();
          var tp = Type.GetType(typeName);
          if (tp == null)
          {
            var map = new TypeMappingEventArgs
                      {
                        TypeName = typeName
                      };
            InvokeMapMissingType(map);
            tp = map.UseType;
          }
          if (!Verbose)
            if (tp == null)
              throw new ArgumentException(string.Format("Cannot reference type {0} in this context", typeName));
          _knownTypes.Add(tp);
        }
        count = rw.ReadInt32();
        for (var i = 0; i < count; i++)
        {
          _propertyIds.Add(rw.ReadString());
        }

        return DeserializeObject(rw, null, instance);
      }
      finally
      {
        _knownTypes = _ktStack.Pop();
        _propertyIds = _piStack.Pop();
        _loadedObjects = _loStack.Pop();
        Verbose = v;
      }
    }

    /// <summary>
    ///   Convert a previously serialized object from a byte array 
    ///   back into a .NET object
    /// </summary>
    /// <param name = "bytes">The data stream for the object</param>
    /// <returns>The rehydrated object represented by the data supplied</returns>
    public static object Deserialize(byte[] bytes)
    {
      using (MemoryStream inputStream = new MemoryStream(bytes))
      {
        return Deserialize(inputStream);
      }
    }

    /// <summary>
    ///   Convert a previously serialized object from a byte array 
    ///   back into a .NET object
    /// </summary>
    /// <param name = "bytes">The data stream for the object</param>
    /// <returns>The rehydrated object represented by the data supplied</returns>
    public static void DeserializeInto(byte[] bytes, object instance)
    {
      using (MemoryStream inputStream = new MemoryStream(bytes))
      {
        Deserialize(inputStream, instance);
      }
    }


    /// <summary>
    ///   Creates a set of stacks on the current thread
    /// </summary>
    private static void CreateStacks()
    {
      if (_piStack == null)
        _piStack = new Stack<List<string>>();
      if (_ktStack == null)
        _ktStack = new Stack<List<Type>>();
      if (_loStack == null)
        _loStack = new Stack<List<object>>();
      if (_soStack == null)
        _soStack = new Stack<Dictionary<object, int>>();
    }

    /// <summary>
    ///   Deserializes an object or primitive from the stream
    /// </summary>
    /// <param name = "reader">The reader of the binary file</param>
    /// <param name = "itemType">The expected type of the item being read (supports compact format)</param>
    /// <returns>The value read from the file</returns>
    /// <remarks>
    ///   The function is supplied with the type of the property that the object was stored in (if known) this enables
    ///   a compact format where types only have to be specified if they differ from the expected one
    /// </remarks>
    private static object DeserializeObject(BinaryReader reader, Type itemType = null, object instance = null)
    {
      var tpId = (ushort)reader.ReadUInt16();
      if (tpId == 0xFFFE)
        return null;

      //Lookup the value type if necessary
      if (tpId != 0xffff || itemType == null)
        itemType = _knownTypes[tpId];

      object obj = null;
      if (itemType != null)
      {
        //Check for custom serialization
        if (Serializers.ContainsKey(itemType))
        {
          //Read the serializer and its data
          var serializer = Serializers[itemType];
          object[] data = DeserializeObject(reader, typeof(object[])) as object[];
          return serializer.Deserialize(data);
        }

        //Check if this is a simple value and read it if so
        if (IsSimpleType(itemType))
        {
          var info = itemType.GetTypeInfo();
          if (info.IsEnum)
          {
            return Enum.Parse(itemType, ReadValue(reader, typeof(int)).ToString(), true);
          }
          return ReadValue(reader, itemType);
        }
      }
      //See if we should lookup this object or create a new one
      var found = reader.ReadChar();
      if (found == 'S') //S is for Seen
        return _loadedObjects[reader.ReadInt32()];
      if (itemType != null)
      {
        //Otherwise create the object
        if (itemType.IsArray)
        {
          int baseCount = reader.ReadInt32();

          if (baseCount == -1)
          {
            return DeserializeMultiDimensionArray(itemType, reader, baseCount);
          }
          else
          {
            return DeserializeArray(itemType, reader, baseCount);
          }
        }

        obj = instance ?? CreateObject(itemType);
        _loadedObjects.Add(obj);
      }
      //Check for collection types)
      //if (obj is IDictionary)
      //    return DeserializeDictionary(obj as IDictionary, itemType, reader);
      if (obj is IList)
        return DeserializeList(obj as IList, itemType, reader);


      //Otherwise we are serializing an object
      return DeserializeObjectAndProperties(obj, itemType, reader);

    }

    /// <summary>
    ///   Deserializes an array of values
    /// </summary>
    /// <param name = "itemType">The type of the array</param>
    /// <param name = "reader">The reader of the stream</param>
    /// <returns>The deserialized array</returns>
    /// <remarks>
    ///   This routine optimizes for arrays of primitives and bytes
    /// </remarks>
    private static object DeserializeArray(Type itemType, BinaryReader reader, int count)
    {
      // If the count is -1 at this point, then it is being called from the
      // deserialization of a multi-dimensional array - so we need
      // to read the size of the array
      if (count == -1)
      {
        count = reader.ReadInt32();
      }

      //Get the expected element type
      var elementType = itemType.GetElementType();
      //Optimize for byte arrays
      if (elementType == typeof(byte))
      {
        var ret = reader.ReadBytes(count);
        _loadedObjects.Add(ret);
        return ret;
      }

      //Create an array of the correct type
      var array = Array.CreateInstance(elementType, count);
      _loadedObjects.Add(array);
      //Check whether the array contains primitives, if it does we don't
      //need to store the type of each member
      if (IsSimpleType(elementType))
        for (var l = 0; l < count; l++)
        {
          array.SetValue(ReadValue(reader, elementType), l);
        }
      else
        for (var l = 0; l < count; l++)
        {
          array.SetValue(DeserializeObject(reader, elementType), l);
        }
      return array;
    }

    /// <summary>
    ///   Deserializes a multi-dimensional array of values
    /// </summary>
    /// <param name = "itemType">The type of the array</param>
    /// <param name = "reader">The reader of the stream</param>
    /// <param name="count">The base size of the multi-dimensional array</param>
    /// <returns>The deserialized array</returns>
    /// <remarks>
    ///   This routine deserializes values serialized on a 'row by row' basis, and
    ///   calls into DeserializeArray to do this
    /// </remarks>
    private static object DeserializeMultiDimensionArray(Type itemType, BinaryReader reader, int count)
    {
      //Read the number of dimensions the array has
      var dimensions = reader.ReadInt32();
      var totalLength = reader.ReadInt32();

      int rowLength = 0;

      // Establish the length of each array element
      // and get the total 'row size'
      int[] lengths = new int[dimensions];
      int[] indices = new int[dimensions];

      for (int item = 0; item < dimensions; item++)
      {
        lengths[item] = reader.ReadInt32();
        rowLength += lengths[item];
        indices[item] = 0;
      }

      int cols = lengths[lengths.Length - 1];
      //int cols = dimensions == 1 ? 1 : lengths[lengths.Length - 1];

      //Get the expected element type
      var elementType = itemType.GetElementType();



      Array sourceArrays = Array.CreateInstance(elementType, lengths);
      DeserializeArrayPart(sourceArrays, 0, indices, itemType, reader);
      return sourceArrays;
    }

    private static void DeserializeArrayPart(Array sourceArrays, int i, int[] indices, Type itemType, BinaryReader binaryReader)
    {
      int length = sourceArrays.GetLength(i);
      for (var l = 0; l < length; l++)
      {
        indices[i] = l;
        if (i != sourceArrays.Rank - 2)
          DeserializeArrayPart(sourceArrays, i + 1, indices, itemType, binaryReader);
        else
        {
          Array sourceArray = (Array)DeserializeArray(itemType, binaryReader, -1);
          int cols = sourceArrays.GetLength(i + 1);
          for (int arrayStartIndex = 0; arrayStartIndex < cols; arrayStartIndex++)
          {
            indices[i + 1] = arrayStartIndex;
            sourceArrays.SetValue(sourceArray.GetValue(arrayStartIndex), indices);
          }
        }
      }
    }

    /// <summary>
    ///   Deserializes a dictionary from storage, handles generic types with storage optimization
    /// </summary>
    /// <param name = "o">The newly created dictionary</param>
    /// <param name = "itemType">The type of the dictionary</param>
    /// <param name = "reader">The binary reader for the current bytes</param>
    /// <returns>The dictionary object updated with the values from storage</returns>
    //private static object DeserializeDictionary(IDictionary o, Type itemType, BinaryReader reader)
    //{
    //    Type keyType = null;
    //    Type valueType = null;
    //    if (itemType.IsGenericType)
    //    {
    //        var types = itemType.GetGenericArguments();
    //        keyType = types[0];
    //        valueType = types[1];
    //    }

    //    var count = reader.ReadInt32();
    //    var list = new List<object>();
    //    for (var i = 0; i < count; i++)
    //    {
    //        list.Add(DeserializeObject(reader, keyType));
    //    }
    //    for (var i = 0; i < count; i++)
    //    {
    //        o[list[i]] = DeserializeObject(reader, valueType);
    //    }
    //    return o;
    //}

    /// <summary>
    ///   Deserialize a list from the data stream
    /// </summary>
    /// <param name = "o">The newly created list</param>
    /// <param name = "itemType">The type of the list</param>
    /// <param name = "reader">The reader for the current bytes</param>
    /// <returns>The list updated with values from the stream</returns>
    private static object DeserializeList(IList o, Type itemType, BinaryReader reader)
    {
      Type valueType = null;
      var info = itemType.GetTypeInfo();
      if (info.IsGenericType)
      {
        var types = info.GenericTypeArguments;
        valueType = types[0];
      }

      var count = reader.ReadInt32();
      var list = new List<object>();
      for (var i = 0; i < count; i++)
      {
        o.Add(DeserializeObject(reader, valueType));
      }
      return o;
    }

    /// <summary>
    ///   Deserializes a class based object that is not a collection, looks for both public properties and fields
    /// </summary>
    /// <param name = "o">The object being deserialized</param>
    /// <param name = "itemType">The type of the object</param>
    /// <param name = "reader">The reader for the current stream of bytes</param>
    /// <returns>The object updated with values from the stream</returns>
    private static object DeserializeObjectAndProperties(object o, Type itemType, BinaryReader reader)
    {
      DeserializeProperties(reader, itemType, o);
      DeserializeFields(reader, itemType, o);
      return o;
    }


    /// <summary>
    ///   Deserializes the properties of an object from the stream
    /// </summary>
    /// <param name = "reader">The reader of the bytes in the stream</param>
    /// <param name = "itemType">The type of the object</param>
    /// <param name = "o">The object to deserialize</param>
    private static void DeserializeProperties(BinaryReader reader, Type itemType, object o)
    {
      //Get the number of properties
      var propCount = reader.ReadByte();
      int length = 0;
      if (Verbose)
        length = reader.ReadInt32();
      if (o == null)
      {
        reader.BaseStream.Seek(length, SeekOrigin.Current);
        return;
      }
      for (var i = 0; i < propCount; i++)
      {
        //Get a property name identifier
        var propId = reader.ReadUInt16();
        //Lookup the name
        var propName = _propertyIds[propId];
        //Use the name to find the type
        var propType = itemType.GetRuntimeProperty(propName);
        //Deserialize the value
        var value = DeserializeObject(reader, propType != null ? propType.PropertyType : null);
        if (propType != null && value != null)
        {
          try
          {
            propType.SetValue(o, value, null);
          }
          catch (Exception)
          {
            //Suppress cases where the old value is no longer compatible with the new property type


          }

        }
      }
    }

    /// <summary>
    ///   Deserializes the fields of an object from the stream
    /// </summary>
    /// <param name = "reader">The reader of the bytes in the stream</param>
    /// <param name = "itemType">The type of the object</param>
    /// <param name = "o">The object to deserialize</param>
    private static void DeserializeFields(BinaryReader reader, Type itemType, object o)
    {
      var fieldCount = reader.ReadByte();
      int length = 0;
      if (Verbose)
        length = reader.ReadInt32();
      if (o == null)
      {
        reader.BaseStream.Seek(length, SeekOrigin.Current);
        return;
      }
      for (var i = 0; i < fieldCount; i++)
      {
        var fieldId = reader.ReadUInt16();
        var fieldName = _propertyIds[fieldId];
        var fieldType = itemType.GetRuntimeFields().Where(p => p.Name == fieldName).FirstOrDefault();
        var value = DeserializeObject(reader, fieldType != null ? fieldType.FieldType : null);
        if (fieldType != null && value != null)
        {
          try
          {
            fieldType.SetValue(o, value);
          }
          catch (Exception)
          {
            //Suppress cases where the old value is no longer compatible with the new property type
          }

        }
      }
    }


    public static void Serialize(object item, Stream outputStream)
    {
      CreateStacks();


      try
      {
        _ktStack.Push(_knownTypes);
        _piStack.Push(_propertyIds);
        _soStack.Push(_seenObjects);

        _propertyIds = new List<string>();
        _knownTypes = new List<Type>();
        _seenObjects = new Dictionary<object, int>();
        var strm = new MemoryStream();
        var wr = new BinaryWriter(strm);
        SerializeObject(item, wr);
        var outputWr = new BinaryWriter(outputStream);
        outputWr.Write("SerV3");
        outputWr.Write(_knownTypes.Count);
        //New, store the verbose property
        outputWr.Write(Verbose);
        foreach (var kt in _knownTypes)
        {
          outputWr.Write(kt.AssemblyQualifiedName);
        }
        outputWr.Write(_propertyIds.Count);
        foreach (var pi in _propertyIds)
        {
          outputWr.Write(pi);
        }
        strm.WriteTo(outputStream);
      }
      finally
      {
        _knownTypes = _ktStack.Pop();
        _propertyIds = _piStack.Pop();
        _seenObjects = _soStack.Pop();
      }

    }

    /// <summary>
    ///   Serialize an object into an array of bytes
    /// </summary>
    /// <param name = "item">The object to serialize</param>
    /// <returns>A byte array representation of the item</returns>
    public static byte[] Serialize(object item)
    {
      using (MemoryStream outputStream = new MemoryStream())
      {
        Serialize(item, outputStream);
        //Reset the verbose mode
        return outputStream.ToArray();
      }
    }

    /// <summary>
    ///   Serialize an object into an array of bytes
    /// </summary>
    /// <param name = "item">The object to serialize</param>
    /// <param name="makeVerbose">Whether the object should be serialized for forwards compatibility</param>
    /// <returns>A byte array representation of the item</returns>
    public static byte[] Serialize(object item, bool makeVerbose)
    {
      using (MemoryStream outputStream = new MemoryStream())
      {
        var v = Verbose;
        Verbose = makeVerbose;
        Serialize(item, outputStream);
        Verbose = v;
        //Reset the verbose mode
        return outputStream.ToArray();
      }
    }
    private static void SerializeObject(object item, BinaryWriter writer, Type propertyType = null)
    {
      if (item == null)
      {
        writer.Write((ushort)0xFFFE);
        return;
      }

      var itemType = item.GetType();
      Debug.Assert(itemType != null);

      //If this isn't a simple type, then this might be a subclass so we need to
      //store the type
      if (propertyType != itemType || Verbose)
      {
        //Write the type identifier
        var tpId = GetTypeId(itemType);
        writer.Write(tpId);
      }
      else
        //Write a dummy identifier
        writer.Write((ushort)0xFFFF);


      //Check for custom serialization
      if (Serializers.ContainsKey(itemType))
      {
        //If we have a custom serializer then use it!
        var serializer = Serializers[itemType];
        var data = serializer.Serialize(item);
        SerializeObject(data, writer, typeof(object[]));
        return;
      }


      //Check for simple types again
      if (IsSimpleType(itemType))
      {
        var info = itemType.GetTypeInfo();
        if (info.IsEnum)
          WriteValue(writer, (int)item);
        else
          WriteValue(writer, item);
        return;
      }

      //Check whether this object has been seen
      if (_seenObjects.ContainsKey(item))
      {
        writer.Write('S');
        writer.Write(_seenObjects[item]);
        return;
      }

      //We are going to serialize an object
      writer.Write('O');
      _seenObjects[item] = _seenObjects.Count;

      //Check for collection types)
      if (item is Array)
      {
        if (((Array)item).Rank == 1)
        {
          SerializeArray(item as Array, itemType, writer);
        }
        else
        {
          SerializeMultiDimensionArray(item as Array, itemType, writer);
        }
        return;
      }
      //if (item is IDictionary)
      //{
      //    SerializeDictionary(item as IDictionary, itemType, writer);
      //    return;
      //}
      if (item is IList)
      {
        SerializeList(item as IList, itemType, writer);
        return;
      }


      //Otherwise we are serializing an object
      SerializeObjectAndProperties(item, itemType, writer);
    }

    private static void SerializeList(IList item, Type tp, BinaryWriter writer)
    {
      var info = tp.GetTypeInfo();
      Type valueType = null;
      //Try to optimize the storage of types based on the type of list
      if (info.IsGenericType)
      {
        var types = info.GenericTypeArguments;
        valueType = types[0];
      }

      writer.Write(item.Count);
      foreach (var val in item)
      {
        SerializeObject(val, writer, valueType);
      }
    }

    //private static void SerializeDictionary(IDictionary item, Type tp, BinaryWriter writer)
    //{
    //    Type keyType = null;
    //    Type valueType = null;
    //    //Try to optimise storage based on the type of dictionary
    //    if (tp.IsGenericType)
    //    {
    //        var types = tp.GetGenericArguments();
    //        keyType = types[0];
    //        valueType = types[1];
    //    }

    //    //Write out the size
    //    writer.Write(item.Count);
    //    //Serialize the pairs
    //    foreach (var key in item.Keys)
    //    {
    //        SerializeObject(key, writer, keyType);
    //    }
    //    foreach (var val in item.Values)
    //    {
    //        SerializeObject(val, writer, valueType);
    //    }
    //}

    private static void SerializeArray(Array item, Type tp, BinaryWriter writer)
    {
      var length = item.Length;

      writer.Write(length);

      var propertyType = tp.GetElementType();
      //Special optimization for arrays of byte
      if (propertyType == typeof(byte))
        writer.Write((byte[])item, 0, length);
      //Special optimization for arrays of simple types
      //which don't need to have the entry type stored
      //for each item
      else if (IsSimpleType(propertyType))
        for (var l = 0; l < length; l++)
        {
          WriteValue(writer, item.GetValue(l));
        }
      else
        for (var l = 0; l < length; l++)
        {
          SerializeObject(item.GetValue(l), writer, propertyType);
        }
    }

    private static void SerializeMultiDimensionArray(Array item, Type tp, BinaryWriter writer)
    {

      // Multi-dimension serializer data is:
      // Int32: Ranks
      // Int32 (x number of ranks): length of array dimension 

      int dimensions = item.Rank;

      var length = item.GetLength(0);

      // Determine the number of cols being populated
      var cols = item.GetLength(item.Rank - 1);

      // Explicitly write this value, to denote that this is a multi-dimensional array
      // so it doesn't break the deserializer when reading values for existing arrays
      writer.Write((int)-1);
      writer.Write(dimensions);
      writer.Write(item.Length);

      var propertyType = tp.GetElementType();
      var indicies = new int[dimensions];

      // Write out the length of each array, if we are dealing with the first array
      for (int arrayStartIndex = 0; arrayStartIndex < dimensions; arrayStartIndex++)
      {
        indicies[arrayStartIndex] = 0;
        writer.Write(item.GetLength(arrayStartIndex));
      }

      SerializeArrayPart(item, 0, indicies, writer);
    }

    private static void SerializeArrayPart(Array item, int i, int[] indices, BinaryWriter writer)
    {
      var length = item.GetLength(i);
      for (var l = 0; l < length; l++)
      {
        indices[i] = l;
        if (i != item.Rank - 2)
          SerializeArrayPart(item, i + 1, indices, writer);
        else
        {
          Type arrayType = item.GetType().GetElementType();
          var cols = item.GetLength(i + 1);

          var baseArray = Array.CreateInstance(arrayType, cols);

          // Convert the whole multi-dimensional array to be 'row' based
          // and serialize using the existing code
          for (int arrayStartIndex = 0; arrayStartIndex < cols; arrayStartIndex++)
          {
            indices[i + 1] = arrayStartIndex;
            baseArray.SetValue(item.GetValue(indices), arrayStartIndex);
          }

          SerializeArray(baseArray, baseArray.GetType(), writer);
        }
      }
    }


    /// <summary>
    ///   Return whether the type specified is a simple type that can be serialized fast
    /// </summary>
    /// <param name = "tp">The type to check</param>
    /// <returns>True if the type is a simple one and can be serialized directly</returns>
    private static bool IsSimpleType(Type tp)
    {
      var info = tp.GetTypeInfo();
      return info.IsPrimitive || tp == typeof(DateTime) || tp == typeof(TimeSpan) || tp == typeof(string) || info.IsEnum || tp == typeof(Guid) || tp == typeof(decimal);
    }

    private static void SerializeObjectAndProperties(object item, Type itemType, BinaryWriter writer)
    {
      lock (Vanilla)
      {
        if (Vanilla.ContainsKey(itemType) == false)
        {

          Vanilla[itemType] = CreateObject(itemType);
        }
      }


      WriteProperties(itemType, item, writer);
      WriteFields(itemType, item, writer);
    }

    private static object CreateObject(Type itemType)
    {
      try
      {
        return Activator.CreateInstance(itemType);
      }
      catch (Exception)
      {
        return null;
      }

    }

    private static void WriteProperties(Type itemType, object item, BinaryWriter writer)
    {
      var propertyStream = new MemoryStream();
      var pw = new BinaryWriter(propertyStream);
      byte propCount = 0;

      //Get the properties of the object
      var properties = GetPropertyInfo(itemType);
      foreach (var property in properties)
      {
        if (IsChecksum && IsLoud)
        {
          Debug.WriteLine(string.Format(" ---->     {0}  on {1}", property.Name, item.ToString()));
        }
        var value = property.GetValue(item, null);
        //Don't store null values
        if (value == null)
          continue;
        //Don't store empty collections
        if (value is ICollection)
          if ((value as ICollection).Count == 0)
            continue;
        //Don't store empty arrays
        if (value is Array)
          if ((value as Array).Length == 0)
            continue;
        //Check whether the value differs from the default
        lock (Vanilla)
        {
          if (value.Equals(property.GetValue(Vanilla[itemType], null)))
            continue;
        }
        //If we get here then we need to store the property
        propCount++;
        pw.Write(GetPropertyDefinitionId(property.Name));
        SerializeObject(value, pw, property.PropertyType);
      }
      writer.Write(propCount);
      if (Verbose)
        writer.Write((int)propertyStream.Length);
      propertyStream.WriteTo(writer.BaseStream);
    }

    private static void WriteFields(Type itemType, object item, BinaryWriter writer)
    {
      var fieldStream = new MemoryStream();
      var fw = new BinaryWriter(fieldStream);
      byte fieldCount = 0;

      //Get the public fields of the object
      var fields = GetFieldInfo(itemType);
      foreach (var field in fields)
      {
        var value = field.GetValue(item);
        //Don't store null values
        if (value == null)
          continue;
        //Don't store empty collections
        if (value is ICollection)
          if ((value as ICollection).Count == 0)
            continue;
        //Don't store empty arrays
        if (value is Array)
          if ((value as Array).Length == 0)
            continue;
        //Check whether the value differs from the default
        lock (Vanilla)
        {
          if (value.Equals(field.GetValue(Vanilla[itemType])))
            continue;
        }
        //if we get here then we need to store the field
        fieldCount++;
        fw.Write(GetPropertyDefinitionId(field.Name));
        SerializeObject(value, fw, field.FieldType);
      }
      writer.Write(fieldCount);
      if (Verbose)
        writer.Write((int)fieldStream.Length);
      fieldStream.WriteTo(writer.BaseStream);
    }

    /// <summary>
    ///   Write a basic untyped value
    /// </summary>
    /// <param name = "writer">The writer to commit byte to</param>
    /// <param name = "value">The value to write</param>
    private static void WriteValue(BinaryWriter writer, object value)
    {
      if (value is string)
        writer.Write((string)value);
      else if (value == null)
        writer.Write("~~NULL~~");
      else if (value is decimal)
      {
        int[] array = Decimal.GetBits((Decimal)value);
        SerializeObject(array, writer, typeof(int[]));
      }
      else if (value is float)
        writer.Write((float)value);
      else if (value is bool)
        writer.Write((bool)value
                         ? 'Y'
                         : 'N');
      else if (value is Guid)
        writer.Write(value.ToString());
      else if (value is DateTime)
        writer.Write(((DateTime)value).Ticks);
      else if (value is TimeSpan)
        writer.Write(((TimeSpan)value).Ticks);
      else if (value is char)
        writer.Write((char)value);
      else if (value is ushort)
        writer.Write((ushort)value);
      else if (value is double)
        writer.Write((double)value);
      else if (value is ulong)
        writer.Write((ulong)value);
      else if (value is int)
        writer.Write((int)value);
      else if (value is uint)
        writer.Write((uint)value);
      else if (value is byte)
        writer.Write((byte)value);
      else if (value is long)
        writer.Write((long)value);
      else if (value is short)
        writer.Write((short)value);
      else if (value is sbyte)
        writer.Write((sbyte)value);
      else
        writer.Write((int)value);
    }

    /// <summary>
    ///   Read a basic value from the stream
    /// </summary>
    /// <param name = "reader">The reader with the stream</param>
    /// <param name = "tp">The type to read</param>
    /// <returns>The hydrated value</returns>
    private static object ReadValue(BinaryReader reader, Type tp)
    {

      if (tp == typeof(string))
      {
        var retString = reader.ReadString();

        return retString == "~~NULL~~"
                   ? null
                   : retString;
      }
      if (tp == typeof(bool))
        return reader.ReadChar() == 'Y';
      if (tp == typeof(decimal))
      {
        var array = DeserializeObject(reader, typeof(int[])) as int[];
        return new Decimal(array);
      }
      if (tp == typeof(DateTime))
        return new DateTime(reader.ReadInt64());
      if (tp == typeof(TimeSpan))
        return new TimeSpan(reader.ReadInt64());
      if (tp == typeof(float))
        return reader.ReadSingle();
      if (tp == typeof(char))
        return reader.ReadChar();
      if (tp == typeof(ushort))
        return reader.ReadUInt16();
      if (tp == typeof(double))
        return reader.ReadDouble();
      if (tp == typeof(ulong))
        return reader.ReadUInt64();
      if (tp == typeof(int))
        return reader.ReadInt32();
      if (tp == typeof(uint))
        return reader.ReadUInt32();
      if (tp == typeof(byte))
        return reader.ReadByte();
      if (tp == typeof(long))
        return reader.ReadInt64();
      if (tp == typeof(short))
        return reader.ReadInt16();
      if (tp == typeof(sbyte))
        return reader.ReadSByte();
      if (tp == typeof(Guid))
        return new Guid(reader.ReadString());
      return reader.ReadInt32();
    }

    /// <summary>
    ///   Logs a type and returns a unique token for it
    /// </summary>
    /// <param name = "tp">The type to retrieve a token for</param>
    /// <returns>A 2 byte token representing the type</returns>
    private static ushort GetTypeId(Type tp)
    {
      var tpId = _knownTypes.IndexOf(tp);

      if (tpId < 0)
      {
        tpId = _knownTypes.Count;
        _knownTypes.Add(tp);
      }
      return (ushort)tpId;
    }
  }
}
